分类
联系方式
  1. 新浪微博
  2. E-mail

BackTrader 海龟交易策略

介绍

海龟交易策略是一种经典的交易系统。

本文策略是在《Backtrader来啦:常见案例汇总》一文中提出的,我进行了复现。

本文只用于数学、编程研究,不提供交易指导。

《Backtrader来啦:常见案例汇总》一文中总结道,策略细节为:

指标计算:20日最高、最低、收盘价计算真实波动 ATR

计算20日最高、最低加,构建唐奇安通道

交易信号:

  • 入场:价格突破20日最高价
  • 加仓:价格继续上涨至 0.5 倍 ATR ,再次加仓,加仓次数不超过 3 次;
  • 止损:价格回落 2 倍 ATR 时止损离场;
  • 止盈:价格突破 10 日最低点时止盈离场;

其中:海龟交易策略之所以称之为交易系统,因为包括了加仓、止盈止损的逻辑在内。

代码实现

具体代码实现参照《Backtrader来啦:常见案例汇总》一文,融入我的量化系统,具体代码如下:

import backtrader as bt

from newstock.data.mongo.mongo_data_manager import MongoDataManager
from newstock.date.stock_date import StockDate
from newstock.market.Exchange import SZSEExchange
from newstock.market.symbol import Symbol
import pandas as pd

"""
海龟交易系统
"""


class TestStrategy(bt.Strategy):
    params = dict(
        N1=20,  # 唐奇安通道上轨的t
        N2=10,  # 唐奇安通道下轨的t
        printlog=False,
    )

    def log(self, txt, dt=None, doprint=False):
        """Logging function for this strategy"""
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print("%s, %s" % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders
        self.order = None
        self.buy_count = 0  # 记录买入次数
        self.last_price = 0  # 记录买入价格

        self.close = self.datas[0].close
        self.high = self.datas[0].high
        self.low = self.datas[0].low

        # 计算唐奇安通道上轨:过去20日的最高价
        self.DonchianH = bt.ind.Highest(self.high(-1), period=self.p.N1, subplot=True)
        # 计算唐奇安通道下轨:过去10日的最低价
        self.DonchianL = bt.ind.Lowest(self.low(-1), period=self.p.N2, subplot=True)
        # 生成唐奇安通道上轨突破:close>DonchianH,取值为1.0;反之为 -1.0
        self.CrossoverH = bt.ind.CrossOver(self.close(0), self.DonchianH, subplot=False)
        # 生成唐奇安通道下轨突破:
        self.CrossoverL = bt.ind.CrossOver(self.close(0), self.DonchianL, subplot=False)
        # 计算 ATR
        self.TR = bt.ind.Max(
            (self.high(0) - self.low(0)),  # 当日最高价-当日最低价
            abs(self.high(0) - self.close(-1)),  # abs(当日最高价−前一日收盘价)
            abs(self.low(0) - self.close(-1)),  # abs(当日最低价-前一日收盘价)
        )
        self.ATR = bt.ind.SimpleMovingAverage(self.TR, period=self.p.N1, subplot=False)

        bt.indicators.MACDHisto(self.datas[0])

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # 交易完成
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

                self.buyprice = order.executed.price  # 买入价格
                self.buycomm = order.executed.comm  # 买入手续费
            elif order.issell():
                self.log(
                    "SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

            self.bar_executed = len(self)  # 买入日期

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        # Write down: no pending order
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log("Close, %.2f" % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # 计算加仓单位,写死10手
        buy_count = 100
        # 入场:价格突破上轨线且空仓时,做多
        if self.position.size == 0:
            if self.CrossoverH > 0 and self.buy_count == 0:
                self.order = self.buy(size=buy_count)
                self.last_price = self.position.price  # 记录买入价格
                self.buy_count = 1  # 记录本次交易价格
        elif self.position.size > 0:
            # 多单加仓:价格上涨了买入价的0.5的ATR且加仓次数少于等于3次
            if (
                self.datas[0].close > self.last_price + 0.5 * self.ATR[0]
                and self.buy_count <= 3
            ):
                self.order = self.buy(size=buy_count)
                self.last_price = self.position.price  # 获取买入价格
                self.buy_count = self.buy_count + 1
            # 多单止损:当价格回落2倍ATR时止损平仓
            elif self.datas[0].close < (self.last_price - 2 * self.ATR[0]):
                self.order = self.sell(size=self.position.size)
                self.buy_count = 0
            # 多单止盈:当价格突破10日最低点时止盈离场 平仓
            elif self.CrossoverL < 0:
                self.order = self.sell(size=self.position.size)
                self.buy_count = 0

    def stop(self):
        self.log(
            "Ending Value %.2f" % (self.broker.getvalue()),
            doprint=True,
        )


if __name__ == "__main__":
    cerebro = bt.Cerebro()

    print("Starting Portfolio Value: %.2f" % cerebro.broker.getvalue())

    mongoManager = MongoDataManager()

    df = mongoManager.getStockPeriodFromDB(
        Symbol(SZSEExchange, "000001"),
        StockDate.today().previousDays(500),
        StockDate.today(),
    )
    df["date"] = pd.to_datetime(df["trade_date"], format="%Y%m%d")
    data = bt.feeds.PandasData(dataname=df, datetime="date")  # type: ignore
    df.dropna()

    # 0.1% ... divide by 100 to remove the %
    cerebro.broker.setcommission(commission=0.001)
    # Python 3.10 修复 module 'collections' has no attribute 'Iterable' 开始
    import collections

    collections.Iterable = collections.abc.Iterable
    # Python 3.10 修复 module 'collections' has no attribute 'Iterable' 完成
    # 策略参数优化
    # cerebro.optstrategy(TestStrategy, maperiod=range(10, 31))
    # 策略运行
    cerebro.addstrategy(TestStrategy)
    cerebro.adddata(data)
    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)
    cerebro.run()
    cerebro.plot(style="bar", volume=False)

    print("Final Portfolio Value: %.2f" % cerebro.broker.getvalue())

其中,在每次买入量中我写死了 size=10,应该是 10 手。

效果

我发现效果并不太好,每次都买在最高点上。止损看起来还行。

代码改进

参照《量化投资2:基于backtrader实现完整海龟法则量化回测》,原来在海龟交易法中,每次加仓的单位是与波动情况挂钩的。引入这部分实现。之后的策略变为:

class TestSizer(bt.Sizer):
    params = (("stake", 1),)

    def _getsizing(self, comminfo, cash, data, isbuy):
        if isbuy:
            return self.p.stake
        position = self.broker.getposition(data)
        if not position.size:
            return 0
        else:
            return position.size
        return self.p.stake


class TestStrategy(bt.Strategy):
    params = dict(
        N1=20,  # 唐奇安通道上轨的t
        N2=10,  # 唐奇安通道下轨的t
        printlog=False,
    )

    def log(self, txt, dt=None, doprint=False):
        """Logging function for this strategy"""
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print("%s, %s" % (dt.isoformat(), txt))

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders
        self.order = None
        self.buy_count = 0  # 记录买入次数
        self.last_price = 0  # 记录买入价格
        self.newstake = 0

        self.close = self.datas[0].close
        self.high = self.datas[0].high
        self.low = self.datas[0].low

        # 计算唐奇安通道上轨:过去20日的最高价
        self.DonchianH = bt.ind.Highest(self.high(-1), period=self.p.N1, subplot=False)
        # 计算唐奇安通道下轨:过去10日的最低价
        self.DonchianL = bt.ind.Lowest(self.low(-1), period=self.p.N2, subplot=False)
        # 生成唐奇安通道上轨突破:close>DonchianH,取值为1.0;反之为 -1.0
        self.CrossoverH = bt.ind.CrossOver(self.close(0), self.DonchianH, subplot=False)
        # 生成唐奇安通道下轨突破:
        self.CrossoverL = bt.ind.CrossOver(self.close(0), self.DonchianL, subplot=False)
        # 计算 ATR
        self.TR = bt.ind.Max(
            (self.high(0) - self.low(0)),  # 当日最高价-当日最低价
            abs(self.high(0) - self.close(-1)),  # abs(当日最高价−前一日收盘价)
            abs(self.low(0) - self.close(-1)),  # abs(当日最低价-前一日收盘价)
        )
        self.ATR = bt.ind.SimpleMovingAverage(self.TR, period=self.p.N1, subplot=False)

        bt.indicators.MACDHisto(self.datas[0])

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # 交易完成
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    "BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

                self.buyprice = order.executed.price  # 买入价格
                self.buycomm = order.executed.comm  # 买入手续费
            elif order.issell():
                self.log(
                    "SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f"
                    % (order.executed.price, order.executed.value, order.executed.comm)
                )

            self.bar_executed = len(self)  # 买入日期

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        # Write down: no pending order
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log("OPERATION PROFIT, GROSS %.2f, NET %.2f" % (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log("Close, %.2f" % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # 入场:价格突破上轨线且空仓时,做多
        if self.CrossoverH > 0 and self.buy_count == 0:
            self.newstake = self.broker.getvalue() * 0.01 / self.ATR
            self.newstake = int(self.newstake / 100) * 100
            self.sizer.p.stake = self.newstake
            self.buy_count = 1
            self.order = self.buy()
        # 加仓:价格上涨了买入价的0.5的ATR且加仓次数少于等于3次
        elif (
            self.datas[0].close > self.last_price + 0.5 * self.ATR[0]
            and self.buy_count > 0
            and self.buy_count < 4
        ):
            self.newstake = self.broker.getvalue() * 0.01 / self.ATR
            self.newstake = int(self.newstake / 100) * 100
            self.sizer.p.stake = self.newstake
            self.buy_count = self.buy_count + 1
            self.order = self.buy()
        # 出场
        elif self.CrossoverL < 0 and self.buy_count > 0:
            self.order = self.sell(size=int(self.position.size))
            self.buy_count = 0
        # 止损
        elif (
            self.datas[0].close < (self.last_price - 2 * self.ATR[0])
            and self.buytime > 0
        ):
            self.order = self.sell(size=int(self.position.size))
            self.buy_count = 0

    def stop(self):
        self.log(
            "Ending Value %.2f" % (self.broker.getvalue()),
            doprint=True,
        )

效果

再次运行,效果如下:

从中可以看到,交易时机点不变,变化的是仓位根据波动性而变。整体上最终效果要比之前好一些。

ATR 周期改为 14

在《【手把手教你】用backtrader量化回测海龟交易策略》一文中,将 ATR 的周期改为了 14,我的执行完后,整体效果变化不明显,图就不贴了。

感悟

开仓条件:入场:价格突破20日最高价,这是一个价格突破策略,但是只适合于价格呈上升趋势的,在图中价格下跌或水平趋势是,总是容易开仓在局部高点。

更换开仓条件

灵感,更改开仓条件,变为均线金叉买入,其它不变。

其中:

  1. 交易变得特别频繁,其实效果也没比双均线好多少
  2. 海龟策略的止损比双均线止损要更加灵敏一些